/*
 +---------------------------------------------------------------------------+
 | Facebook Development Platform Java Client                                 |
 +---------------------------------------------------------------------------+
 | Copyright (c) 2007-2008 Facebook, Inc.                                    |
 | All rights reserved.                                                      |
 |                                                                           |
 | Redistribution and use in source and binary forms, with or without        |
 | modification, are permitted provided that the following conditions        |
 | are met:                                                                  |
 |                                                                           |
 | 1. Redistributions of source code must retain the above copyright         |
 |    notice, this list of conditions and the following disclaimer.          |
 | 2. Redistributions in binary form must reproduce the above copyright      |
 |    notice, this list of conditions and the following disclaimer in the    |
 |    documentation and/or other materials provided with the distribution.   |
 |                                                                           |
 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR      |
 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.   |
 | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,          |
 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT  |
 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY     |
 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT       |
 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF  |
 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.         |
 +---------------------------------------------------------------------------+
 | For help with this library, contact developers-help@facebook.com          |
 +---------------------------------------------------------------------------+
 */
package com.facebook.api;

import java.io.BufferedInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;

/**
 * Base class for interacting with the Facebook Application Programming Interface (API). Most
 * Facebook API methods map directly to function calls of this class. <br/> Instances of
 * FacebookRestClient should be initialized via calls to {@link #auth_createToken}, followed by
 * {@link #auth_getSession}. <br/> For continually updated documentation, please refer to the <a
 * href="http://wiki.developers.facebook.com/index.php/API"> Developer Wiki</a>.
 */
public abstract class ExtensibleClient<T> implements IFacebookRestClient<T> {
    public static URL SERVER_URL = null;
    public static URL HTTPS_SERVER_URL = null;
    static {
        try {
            SERVER_URL = new URL(SERVER_ADDR);
            HTTPS_SERVER_URL = new URL(HTTPS_SERVER_ADDR);
        } catch (MalformedURLException e) {
            System.err.println("MalformedURLException: " + e.getMessage());
            System.exit(1);
        }
    }
    protected final String _secret;
    protected final String _apiKey;
    protected final URL _serverUrl;
    protected String _sessionKey;
    protected boolean _isDesktop = false;
    protected int _userId = -1;
    /**
     * filled in when session is established only used for desktop apps
     */
    protected String _sessionSecret;
    /**
     * The number of parameters required for every request.
     * 
     * @see #callMethod(IFacebookMethod,Collection)
     */
    public static int NUM_AUTOAPPENDED_PARAMS = 6;
    private static boolean DEBUG = false;
    protected Boolean _debug = null;
    protected File _uploadFile = null;
    protected static final String CRLF = "\r\n";
    protected static final String PREF = "--";
    protected static final int UPLOAD_BUFFER_SIZE = 512;
    public static final String MARKETPLACE_STATUS_DEFAULT = "DEFAULT";
    public static final String MARKETPLACE_STATUS_NOT_SUCCESS = "NOT_SUCCESS";
    public static final String MARKETPLACE_STATUS_SUCCESS = "SUCCESS";

    protected ExtensibleClient(URL serverUrl, String apiKey, String secret, String sessionKey) {
        _sessionKey = sessionKey;
        _apiKey = apiKey;
        _secret = secret;
        _serverUrl = (null != serverUrl)? serverUrl: SERVER_URL;
    }

    /**
     * The response format in which results to FacebookMethod calls are returned
     * 
     * @return the format: either XML, JSON, or null (API default)
     */
    public String getResponseFormat() {
        return null;
    }

    /**
     * Retrieves whether two users are friends.
     * 
     * @param userId1
     * @param userId2
     * @return T
     * @see <a href="http://wiki.developers.facebook.com/index.php/Friends.areFriends"> Developers
     *      Wiki: Friends.areFriends</a>
     */
    public T friends_areFriends(int userId1, int userId2) throws FacebookException, IOException {
        return this.callMethod(FacebookMethod.FRIENDS_ARE_FRIENDS, new Pair<String,CharSequence>(
                "uids1", Integer.toString(userId1)), new Pair<String,CharSequence>("uids2", Integer
                .toString(userId2)));
    }

    /**
     * Retrieves whether pairs of users are friends. Returns whether the first user in
     * <code>userIds1</code> is friends with the first user in <code>userIds2</code>, the
     * second user in <code>userIds1</code> is friends with the second user in
     * <code>userIds2</code>, etc.
     * 
     * @param userIds1
     * @param userIds2
     * @return T
     * @throws IllegalArgumentException if one of the collections is null, or empty, or if the
     *             collection sizes differ.
     * @see <a href="http://wiki.developers.facebook.com/index.php/Friends.areFriends"> Developers
     *      Wiki: Friends.areFriends</a>
     */
    public T friends_areFriends(Collection<Integer> userIds1, Collection<Integer> userIds2)
            throws FacebookException, IOException {
        if (userIds1 == null || userIds2 == null || userIds1.isEmpty() || userIds2.isEmpty()) {
            throw new IllegalArgumentException(
                    "Collections passed to friends_areFriends should not be null or empty");
        }
        if (userIds1.size() != userIds2.size()) {
            throw new IllegalArgumentException(String.format(
                    "Collections should be same size: got userIds1: %d elts; userIds2: %d elts",
                    userIds1.size(), userIds2.size()));
        }
        return this.callMethod(FacebookMethod.FRIENDS_ARE_FRIENDS, new Pair<String,CharSequence>(
                "uids1", delimit(userIds1)), new Pair<String,CharSequence>("uids2",
                delimit(userIds2)));
    }

    /**
     * Gets the FBML for a user's profile, including the content for both the profile box and the
     * profile actions.
     * 
     * @param userId the user whose profile FBML to set
     * @return a T containing FBML markup
     */
    public T profile_getFBML(Integer userId) throws FacebookException, IOException {
        return this.callMethod(FacebookMethod.PROFILE_GET_FBML, new Pair<String,CharSequence>(
                "uid", Integer.toString(userId)));
    }

    /**
     * Recaches the referenced url.
     * 
     * @param url string representing the URL to refresh
     * @return boolean indicating whether the refresh succeeded
     */
    public boolean fbml_refreshRefUrl(String url) throws FacebookException, IOException {
        return fbml_refreshRefUrl(new URL(url));
    }

    /**
     * Helper function: assembles the parameters used by feed_publishActionOfUser and
     * feed_publishStoryToUser
     * 
     * @param feedMethod feed_publishStoryToUser / feed_publishActionOfUser
     * @param title title of the story
     * @param body body of the story
     * @param images optional images to be included in he story
     * @param priority
     * @return whether the call to <code>feedMethod</code> was successful
     */
    protected boolean feedHandler(IFacebookMethod feedMethod, CharSequence title,
            CharSequence body, Collection<IFeedImage> images, Integer priority)
            throws FacebookException, IOException {
        ArrayList<Pair<String,CharSequence>> params = new ArrayList<Pair<String,CharSequence>>(
                feedMethod.numParams());
        params.add(new Pair<String,CharSequence>("title", title));
        if (null != body)
            params.add(new Pair<String,CharSequence>("body", body));
        if (null != priority)
            params.add(new Pair<String,CharSequence>("priority", priority.toString()));
        handleFeedImages(params, images);
        return extractBoolean(this.callMethod(feedMethod, params));
    }

    /**
     * Adds image parameters to a list of parameters
     * 
     * @param params
     * @param images
     */
    protected void handleFeedImages(List<Pair<String,CharSequence>> params,
            Collection<IFeedImage> images) {
        if (images != null && images.size() > 4) {
            throw new IllegalArgumentException("At most four images are allowed, got "
                    + Integer.toString(images.size()));
        }
        if (null != images && !images.isEmpty()) {
            int image_count = 0;
            for (IFeedImage image: images) {
                ++image_count;
                String imageUrl = image.getImageUrlString();
                assert null != imageUrl && "".equals(imageUrl) : "Image URL must be provided";
                params.add(new Pair<String,CharSequence>(String.format("image_%d", image_count),
                        image.getImageUrlString()));
                assert null != image.getLinkUrl() : "Image link URL must be provided";
                params.add(new Pair<String,CharSequence>(String
                        .format("image_%d_link", image_count), image.getLinkUrl().toString()));
            }
        }
    }

    /**
     * Publish the notification of an action taken by a user to newsfeed.
     * 
     * @param title the title of the feed story (up to 60 characters, excluding tags)
     * @param body (optional) the body of the feed story (up to 200 characters, excluding tags)
     * @param images (optional) up to four pairs of image URLs and (possibly null) link URLs
     * @return whether the story was successfully published; false in case of permission error
     * @see <a href="http://wiki.developers.facebook.com/index.php/Feed.publishActionOfUser">
     *      Developers Wiki: Feed.publishActionOfUser</a>
     */
    public boolean feed_publishActionOfUser(CharSequence title, CharSequence body,
            Collection<IFeedImage> images) throws FacebookException, IOException {
        return feedHandler(FacebookMethod.FEED_PUBLISH_ACTION_OF_USER, title, body, images, null);
    }

    /**
     * Publish the notification of an action taken by a user to newsfeed.
     * 
     * @param title the title of the feed story (up to 60 characters, excluding tags)
     * @param body (optional) the body of the feed story (up to 200 characters, excluding tags)
     * @return whether the story was successfully published; false in case of permission error
     * @see <a href="http://wiki.developers.facebook.com/index.php/Feed.publishActionOfUser">
     *      Developers Wiki: Feed.publishActionOfUser</a>
     */
    public boolean feed_publishActionOfUser(CharSequence title, CharSequence body)
            throws FacebookException, IOException {
        return feed_publishActionOfUser(title, body, null);
    }

    /**
     * Call this function to retrieve the session information after your user has logged in.
     * 
     * @param authToken the token returned by auth_createToken or passed back to your callback_url.
     */
    public abstract String auth_getSession(String authToken) throws FacebookException, IOException;

    /**
     * Publish a story to the logged-in user's newsfeed.
     * 
     * @param title the title of the feed story
     * @param body the body of the feed story
     * @return whether the story was successfully published; false in case of permission error
     * @see <a href="http://wiki.developers.facebook.com/index.php/Feed.publishStoryToUser">
     *      Developers Wiki: Feed.publishStoryToUser</a>
     */
    public boolean feed_publishStoryToUser(CharSequence title, CharSequence body)
            throws FacebookException, IOException {
        return feed_publishStoryToUser(title, body, null, null);
    }

    /**
     * Publish a story to the logged-in user's newsfeed.
     * 
     * @param title the title of the feed story
     * @param body the body of the feed story
     * @param images (optional) up to four pairs of image URLs and (possibly null) link URLs
     * @return whether the story was successfully published; false in case of permission error
     * @see <a href="http://wiki.developers.facebook.com/index.php/Feed.publishStoryToUser">
     *      Developers Wiki: Feed.publishStoryToUser</a>
     */
    public boolean feed_publishStoryToUser(CharSequence title, CharSequence body,
            Collection<IFeedImage> images) throws FacebookException, IOException {
        return feed_publishStoryToUser(title, body, images, null);
    }

    /**
     * Publish a story to the logged-in user's newsfeed.
     * 
     * @param title the title of the feed story
     * @param body the body of the feed story
     * @param priority
     * @return whether the story was successfully published; false in case of permission error
     * @see <a href="http://wiki.developers.facebook.com/index.php/Feed.publishStoryToUser">
     *      Developers Wiki: Feed.publishStoryToUser</a>
     */
    public boolean feed_publishStoryToUser(CharSequence title, CharSequence body, Integer priority)
            throws FacebookException, IOException {
        return feed_publishStoryToUser(title, body, null, priority);
    }

    /**
     * Publish a story to the logged-in user's newsfeed.
     * 
     * @param title the title of the feed story
     * @param body the body of the feed story
     * @param images (optional) up to four pairs of image URLs and (possibly null) link URLs
     * @param priority
     * @return whether the story was successfully published; false in case of permission error
     * @see <a href="http://wiki.developers.facebook.com/index.php/Feed.publishStoryToUser">
     *      Developers Wiki: Feed.publishStoryToUser</a>
     */
    public boolean feed_publishStoryToUser(CharSequence title, CharSequence body,
            Collection<IFeedImage> images, Integer priority) throws FacebookException, IOException {
        return feedHandler(FacebookMethod.FEED_PUBLISH_STORY_TO_USER, title, body, images, priority);
    }

    /**
     * Publishes a Mini-Feed story describing an action taken by a user, and publishes aggregating
     * News Feed stories to the friends of that user. Stories are identified as being combinable if
     * they have matching templates and substituted values.
     * 
     * @param actorId deprecated
     * @param titleTemplate markup (up to 60 chars, tags excluded) for the feed story's title
     *            section. Must include the token <code>{actor}</code>.
     * @return whether the action story was successfully published; false in case of a permission
     *         error
     * @see <a href="http://wiki.developers.facebook.com/index.php/Feed.publishTemplatizedAction">
     *      Developers Wiki: Feed.publishTemplatizedAction</a>
     * @see <a href="http://developers.facebook.com/tools.php?feed"> Developers Resources: Feed
     *      Preview Console </a>
     * @deprecated since 01/18/2008
     */
    public boolean feed_publishTemplatizedAction(Integer actorId, CharSequence titleTemplate)
            throws FacebookException, IOException {
        if (null != actorId && actorId != this._userId) {
            throw new IllegalArgumentException("Actor ID parameter is deprecated");
        }
        return feed_publishTemplatizedAction(titleTemplate, null, null, null, null, null, null, /* pageActorId */
                null);
    }

    /**
     * Publishes a Mini-Feed story describing an action taken by the logged-in user, and publishes
     * aggregating News Feed stories to their friends. Stories are identified as being combinable if
     * they have matching templates and substituted values.
     * 
     * @param titleTemplate markup (up to 60 chars, tags excluded) for the feed story's title
     *            section. Must include the token <code>{actor}</code>.
     * @return whether the action story was successfully published; false in case of a permission
     *         error
     * @see <a href="http://wiki.developers.facebook.com/index.php/Feed.publishTemplatizedAction">
     *      Developers Wiki: Feed.publishTemplatizedAction</a>
     * @see <a href="http://developers.facebook.com/tools.php?feed"> Developers Resources: Feed
     *      Preview Console </a>
     */
    public boolean feed_publishTemplatizedAction(CharSequence titleTemplate)
            throws FacebookException, IOException {
        return feed_publishTemplatizedAction(titleTemplate, null, null, null, null, null, null, /* pageActorId */
                null);
    }

    /**
     * Publishes a Mini-Feed story describing an action taken by the logged-in user (or, if
     * <code>pageActorId</code> is provided, page), and publishes aggregating News Feed stories to
     * the user's friends/page's fans. Stories are identified as being combinable if they have
     * matching templates and substituted values.
     * 
     * @param titleTemplate markup (up to 60 chars, tags excluded) for the feed story's title
     *            section. Must include the token <code>{actor}</code>.
     * @param pageActorId (optional) the ID of the page into whose mini-feed the story is being
     *            published
     * @return whether the action story was successfully published; false in case of a permission
     *         error
     * @see <a href="http://wiki.developers.facebook.com/index.php/Feed.publishTemplatizedAction">
     *      Developers Wiki: Feed.publishTemplatizedAction</a>
     * @see <a href="http://developers.facebook.com/tools.php?feed"> Developers Resources: Feed
     *      Preview Console </a>
     */
    public boolean feed_publishTemplatizedAction(CharSequence titleTemplate, Long pageActorId)
            throws FacebookException, IOException {
        return feed_publishTemplatizedAction(titleTemplate, null, null, null, null, null, null,
                pageActorId);
    }

    /**
     * Publishes a Mini-Feed story describing an action taken by the logged-in user, and publishes
     * aggregating News Feed stories to their friends. Stories are identified as being combinable if
     * they have matching templates and substituted values.
     * 
     * @param actorId deprecated.
     * @param titleTemplate markup (up to 60 chars, tags excluded) for the feed story's title
     *            section. Must include the token <code>{actor}</code>.
     * @param titleData (optional) contains token-substitution mappings for tokens that appear in
     *            titleTemplate. Should not contain mappings for the <code>{actor}</code> or
     *            <code>{target}</code> tokens. Required if tokens other than <code>{actor}</code>
     *            or <code>{target}</code> appear in the titleTemplate.
     * @param bodyTemplate (optional) markup to be displayed in the feed story's body section. can
     *            include tokens, of the form <code>{token}</code>, to be substituted using
     *            bodyData.
     * @param bodyData (optional) contains token-substitution mappings for tokens that appear in
     *            bodyTemplate. Required if the bodyTemplate contains tokens other than
     *            <code>{actor}</code> and <code>{target}</code>.
     * @param bodyGeneral (optional) additional body markup that is not aggregated. If multiple
     *            instances of this templated story are combined together, the markup in the
     *            bodyGeneral of one of their stories may be displayed.
     * @param targetIds The user ids of friends of the actor, used for stories about a direct action
     *            between the actor and these targets of his/her action. Required if either the
     *            titleTemplate or bodyTemplate includes the token <code>{target}</code>.
     * @param images (optional) additional body markup that is not aggregated. If multiple instances
     *            of this templated story are combined together, the markup in the bodyGeneral of
     *            one of their stories may be displayed.
     * @return whether the action story was successfully published; false in case of a permission
     *         error
     * @see <a href="http://wiki.developers.facebook.com/index.php/Feed.publishTemplatizedAction">
     *      Developers Wiki: Feed.publishTemplatizedAction</a>
     * @see <a href="http://developers.facebook.com/tools.php?feed"> Developers Resources: Feed
     *      Preview Console </a>
     * @deprecated since 01/18/2008
     */
    public boolean feed_publishTemplatizedAction(Integer actorId, CharSequence titleTemplate,
            Map<String,CharSequence> titleData, CharSequence bodyTemplate,
            Map<String,CharSequence> bodyData, CharSequence bodyGeneral,
            Collection<Integer> targetIds, Collection<IFeedImage> images) throws FacebookException,
            IOException {
        return feed_publishTemplatizedAction(titleTemplate, titleData, bodyTemplate, bodyData,
                bodyGeneral, targetIds, images, /* pageActorId */null);
    }

    /**
     * Publishes a Mini-Feed story describing an action taken by the logged-in user (or, if
     * <code>pageActorId</code> is provided, page), and publishes aggregating News Feed stories to
     * the user's friends/page's fans. Stories are identified as being combinable if they have
     * matching templates and substituted values.
     * 
     * @param titleTemplate markup (up to 60 chars, tags excluded) for the feed story's title
     *            section. Must include the token <code>{actor}</code>.
     * @param titleData (optional) contains token-substitution mappings for tokens that appear in
     *            titleTemplate. Should not contain mappings for the <code>{actor}</code> or
     *            <code>{target}</code> tokens. Required if tokens other than <code>{actor}</code>
     *            or <code>{target}</code> appear in the titleTemplate.
     * @param bodyTemplate (optional) markup to be displayed in the feed story's body section. can
     *            include tokens, of the form <code>{token}</code>, to be substituted using
     *            bodyData.
     * @param bodyData (optional) contains token-substitution mappings for tokens that appear in
     *            bodyTemplate. Required if the bodyTemplate contains tokens other than
     *            <code>{actor}</code> and <code>{target}</code>.
     * @param bodyGeneral (optional) additional body markup that is not aggregated. If multiple
     *            instances of this templated story are combined together, the markup in the
     *            bodyGeneral of one of their stories may be displayed.
     * @param targetIds The user ids of friends of the actor, used for stories about a direct action
     *            between the actor and these targets of his/her action. Required if either the
     *            titleTemplate or bodyTemplate includes the token <code>{target}</code>.
     * @param images (optional) additional body markup that is not aggregated. If multiple instances
     *            of this templated story are combined together, the markup in the bodyGeneral of
     *            one of their stories may be displayed.
     * @param pageActorId (optional) the ID of the page into whose mini-feed the story is being
     *            published
     * @return whether the action story was successfully published; false in case of a permission
     *         error
     * @see <a href="http://wiki.developers.facebook.com/index.php/Feed.publishTemplatizedAction">
     *      Developers Wiki: Feed.publishTemplatizedAction</a>
     * @see <a href="http://developers.facebook.com/tools.php?feed"> Developers Resources: Feed
     *      Preview Console </a>
     */
    public boolean feed_publishTemplatizedAction(CharSequence titleTemplate,
            Map<String,CharSequence> titleData, CharSequence bodyTemplate,
            Map<String,CharSequence> bodyData, CharSequence bodyGeneral,
            Collection<Integer> targetIds, Collection<IFeedImage> images, Long pageActorId)
            throws FacebookException, IOException {
        assert null != titleTemplate && !"".equals(titleTemplate);
        FacebookMethod method = FacebookMethod.FEED_PUBLISH_TEMPLATIZED_ACTION;
        ArrayList<Pair<String,CharSequence>> params = new ArrayList<Pair<String,CharSequence>>(
                method.numParams());
        params.add(new Pair<String,CharSequence>("title_template", titleTemplate));
        if (null != titleData && !titleData.isEmpty()) {
            JSONObject titleDataJson = new JSONObject();
            titleDataJson.putAll(titleData);
            params.add(new Pair<String,CharSequence>("title_data", titleDataJson.toString()));
        }
        if (null != bodyTemplate && !"".equals(bodyTemplate)) {
            params.add(new Pair<String,CharSequence>("body_template", bodyTemplate));
            if (null != bodyData && !bodyData.isEmpty()) {
                JSONObject bodyDataJson = new JSONObject();
                bodyDataJson.putAll(bodyData);
                params.add(new Pair<String,CharSequence>("body_data", bodyDataJson.toString()));
            }
        }
        if (null != bodyGeneral && !"".equals(bodyGeneral)) {
            params.add(new Pair<String,CharSequence>("body_general", bodyGeneral));
        }
        if (null != targetIds && !targetIds.isEmpty()) {
            params.add(new Pair<String,CharSequence>("target_ids", delimit(targetIds)));
        }
        if (null != pageActorId) {
            params.add(new Pair<String,CharSequence>("page_actor_id", pageActorId.toString()));
        }
        handleFeedImages(params, images);
        return extractBoolean(this.callMethod(method, params));
    }

    /**
     * Retrieves the membership list of a group
     * 
     * @param groupId the group id
     * @return a T containing four membership lists of 'members', 'admins', 'officers', and
     *         'not_replied'
     */
    public T groups_getMembers(Number groupId) throws FacebookException, IOException {
        assert (null != groupId);
        return this.callMethod(FacebookMethod.GROUPS_GET_MEMBERS, new Pair<String,CharSequence>(
                "gid", groupId.toString()));
    }

    private static String encode(CharSequence target) {
        String result = target.toString();
        try {
            result = URLEncoder.encode(result, "UTF8");
        } catch (UnsupportedEncodingException e) {
            System.err.printf("Unsuccessful attempt to encode '%s' into UTF8", result);
        }
        return result;
    }

    /**
     * Retrieves the membership list of an event
     * 
     * @param eventId event id
     * @return T consisting of four membership lists corresponding to RSVP status, with keys
     *         'attending', 'unsure', 'declined', and 'not_replied'
     */
    public T events_getMembers(Number eventId) throws FacebookException, IOException {
        assert (null != eventId);
        return this.callMethod(FacebookMethod.EVENTS_GET_MEMBERS, new Pair<String,CharSequence>(
                "eid", eventId.toString()));
    }

    /**
     * Retrieves the friends of the currently logged in user, who are also users of the calling
     * application.
     * 
     * @return array of friends
     */
    public T friends_getAppUsers() throws FacebookException, IOException {
        return this.callMethod(FacebookMethod.FRIENDS_GET_APP_USERS);
    }

    /**
     * Retrieves the results of a Facebook Query Language query
     * 
     * @param query : the FQL query statement
     * @return varies depending on the FQL query
     */
    public T fql_query(CharSequence query) throws FacebookException, IOException {
        assert (null != query);
        return this.callMethod(FacebookMethod.FQL_QUERY, new Pair<String,CharSequence>("query",
                query));
    }

    private String generateSignature(List<String> params, boolean requiresSession) {
        String secret = (isDesktop() && requiresSession)? this._sessionSecret: this._secret;
        return FacebookSignatureUtil.generateSignature(params, secret);
    }

    public static void setDebugAll(boolean isDebug) {
        ExtensibleClient.DEBUG = isDebug;
    }

    private static CharSequence delimit(Collection iterable) {
        // could add a thread-safe version that uses StringBuffer as well
        if (iterable == null || iterable.isEmpty())
            return null;
        StringBuilder buffer = new StringBuilder();
        boolean notFirst = false;
        for (Object item: iterable) {
            if (notFirst)
                buffer.append(",");
            else
                notFirst = true;
            buffer.append(item.toString());
        }
        return buffer;
    }

    /**
     * Call the specified method, with the given parameters, and return a DOM tree with the results.
     * 
     * @param method the fieldName of the method
     * @param paramPairs a list of arguments to the method
     * @throws Exception with a description of any errors given to us by the server.
     */
    protected T callMethod(IFacebookMethod method, Pair<String,CharSequence>... paramPairs)
            throws FacebookException, IOException {
        return callMethod(method, Arrays.asList(paramPairs));
    }

    /**
     * Used to retrieve photo objects using the search parameters (one or more of the parameters
     * must be provided).
     * 
     * @param albumId retrieve from photos from this album (optional)
     * @param photoIds retrieve from this list of photos (optional)
     * @return an T of photo objects.
     * @see #photos_get(Integer, Long, Collection)
     * @see <a href="http://wiki.developers.facebook.com/index.php/Photos.get"> Developers Wiki:
     *      Photos.get</a>
     */
    public T photos_get(Long albumId, Collection<Long> photoIds) throws FacebookException,
            IOException {
        return photos_get( /* subjId */null, albumId, photoIds);
    }

    /**
     * Used to retrieve photo objects using the search parameters (one or more of the parameters
     * must be provided).
     * 
     * @param photoIds retrieve from this list of photos (optional)
     * @return an T of photo objects.
     * @see #photos_get(Integer, Long, Collection)
     * @see <a href="http://wiki.developers.facebook.com/index.php/Photos.get"> Developers Wiki:
     *      Photos.get</a>
     */
    public T photos_get(Collection<Long> photoIds) throws FacebookException, IOException {
        return photos_get( /* subjId */null, /* albumId */null, photoIds);
    }

    /**
     * Used to retrieve photo objects using the search parameters (one or more of the parameters
     * must be provided).
     * 
     * @param subjId retrieve from photos associated with this user (optional).
     * @param albumId retrieve from photos from this album (optional)
     * @return an T of photo objects.
     * @see #photos_get(Integer, Long, Collection)
     * @see <a href="http://wiki.developers.facebook.com/index.php/Photos.get"> Developers Wiki:
     *      Photos.get</a>
     */
    public T photos_get(Integer subjId, Long albumId) throws FacebookException, IOException {
        return photos_get(subjId, albumId, /* photoIds */null);
    }

    /**
     * Used to retrieve photo objects using the search parameters (one or more of the parameters
     * must be provided).
     * 
     * @param subjId retrieve from photos associated with this user (optional).
     * @param photoIds retrieve from this list of photos (optional)
     * @return an T of photo objects.
     * @see #photos_get(Integer, Long, Collection)
     * @see <a href="http://wiki.developers.facebook.com/index.php/Photos.get"> Developers Wiki:
     *      Photos.get</a>
     */
    public T photos_get(Integer subjId, Collection<Long> photoIds) throws FacebookException,
            IOException {
        return photos_get(subjId, /* albumId */null, photoIds);
    }

    /**
     * Used to retrieve photo objects using the search parameters (one or more of the parameters
     * must be provided).
     * 
     * @param subjId retrieve from photos associated with this user (optional).
     * @return an T of photo objects.
     * @see #photos_get(Integer, Long, Collection)
     * @see <a href="http://wiki.developers.facebook.com/index.php/Photos.get"> Developers Wiki:
     *      Photos.get</a>
     */
    public T photos_get(Integer subjId) throws FacebookException, IOException {
        return photos_get(subjId, /* albumId */null, /* photoIds */null);
    }

    /**
     * Used to retrieve photo objects using the search parameters (one or more of the parameters
     * must be provided).
     * 
     * @param albumId retrieve from photos from this album (optional)
     * @return an T of photo objects.
     * @see #photos_get(Integer, Long, Collection)
     * @see <a href="http://wiki.developers.facebook.com/index.php/Photos.get"> Developers Wiki:
     *      Photos.get</a>
     */
    public T photos_get(Long albumId) throws FacebookException, IOException {
        return photos_get(/* subjId */null, albumId, /* photoIds */null);
    }

    /**
     * Used to retrieve photo objects using the search parameters (one or more of the parameters
     * must be provided).
     * 
     * @param subjId retrieve from photos associated with this user (optional).
     * @param albumId retrieve from photos from this album (optional)
     * @param photoIds retrieve from this list of photos (optional)
     * @return an T of photo objects.
     * @see <a href="http://wiki.developers.facebook.com/index.php/Photos.get"> Developers Wiki:
     *      Photos.get</a>
     */
    public T photos_get(Integer subjId, Long albumId, Collection<Long> photoIds)
            throws FacebookException, IOException {
        ArrayList<Pair<String,CharSequence>> params = new ArrayList<Pair<String,CharSequence>>(
                FacebookMethod.PHOTOS_GET.numParams());
        boolean hasUserId = null != subjId && 0 != subjId;
        boolean hasAlbumId = null != albumId && 0 != albumId;
        boolean hasPhotoIds = null != photoIds && !photoIds.isEmpty();
        if (!hasUserId && !hasAlbumId && !hasPhotoIds) {
            throw new IllegalArgumentException(
                    "At least one of photoIds, albumId, or subjId must be provided");
        }
        if (hasUserId)
            params.add(new Pair<String,CharSequence>("subj_id", Integer.toString(subjId)));
        if (hasAlbumId)
            params.add(new Pair<String,CharSequence>("aid", Long.toString(albumId)));
        if (hasPhotoIds)
            params.add(new Pair<String,CharSequence>("pids", delimit(photoIds)));
        return this.callMethod(FacebookMethod.PHOTOS_GET, params);
    }

    /**
     * Retrieves the requested info fields for the requested set of users.
     * 
     * @param userIds a collection of user IDs for which to fetch info
     * @param fields a set of strings describing the info fields desired, such as "last_name", "sex"
     * @return a T consisting of a list of users, with each user element containing the requested
     *         fields.
     */
    public T users_getInfo(Collection<Integer> userIds, Set<CharSequence> fields)
            throws FacebookException, IOException {
        // assertions test for invalid params
        if (null == userIds) {
            throw new IllegalArgumentException("userIds cannot be null");
        }
        if (fields == null || fields.isEmpty()) {
            throw new IllegalArgumentException("fields should not be empty");
        }
        return this.callMethod(FacebookMethod.USERS_GET_INFO, new Pair<String,CharSequence>("uids",
                delimit(userIds)), new Pair<String,CharSequence>("fields", delimit(fields)));
    }

    /**
     * Retrieves the tags for the given set of photos.
     * 
     * @param photoIds The list of photos from which to extract photo tags.
     * @return the created album
     */
    public T photos_getTags(Collection<Long> photoIds) throws FacebookException, IOException {
        return this.callMethod(FacebookMethod.PHOTOS_GET_TAGS, new Pair<String,CharSequence>(
                "pids", delimit(photoIds)));
    }

    /**
     * Retrieves the groups associated with a user
     * 
     * @param userId Optional: User associated with groups. A null parameter will default to the
     *            session user.
     * @param groupIds Optional: group ids to query. A null parameter will get all groups for the
     *            user.
     * @return array of groups
     */
    public T groups_get(Integer userId, Collection<Long> groupIds) throws FacebookException,
            IOException {
        boolean hasGroups = (null != groupIds && !groupIds.isEmpty());
        if (null != userId)
            return hasGroups? this.callMethod(FacebookMethod.GROUPS_GET,
                    new Pair<String,CharSequence>("uid", userId.toString()),
                    new Pair<String,CharSequence>("gids", delimit(groupIds))): this.callMethod(
                    FacebookMethod.GROUPS_GET, new Pair<String,CharSequence>("uid", userId
                            .toString()));
        else
            return hasGroups? this.callMethod(FacebookMethod.GROUPS_GET,
                    new Pair<String,CharSequence>("gids", delimit(groupIds))): this
                    .callMethod(FacebookMethod.GROUPS_GET);
    }

    /**
     * Call the specified method, with the given parameters, and return a DOM tree with the results.
     * 
     * @param method the fieldName of the method
     * @param paramPairs a list of arguments to the method
     * @throws Exception with a description of any errors given to us by the server.
     */
    protected T callMethod(IFacebookMethod method, Collection<Pair<String,CharSequence>> paramPairs)
            throws FacebookException, IOException {
        HashMap<String,CharSequence> params = new HashMap<String,CharSequence>(2 * method
                .numTotalParams());
        params.put("method", method.methodName());
        params.put("api_key", _apiKey);
        params.put("v", TARGET_API_VERSION);
        String format = getResponseFormat();
        if (null != format) {
            params.put("format", format);
        }
        if (method.requiresSession()) {
            params.put("call_id", Long.toString(System.currentTimeMillis()));
            params.put("session_key", _sessionKey);
        }
        CharSequence oldVal;
        for (Pair<String,CharSequence> p: paramPairs) {
            oldVal = params.put(p.first, p.second);
            if (oldVal != null)
                System.err.printf("For parameter %s, overwrote old value %s with new value %s.",
                        p.first, oldVal, p.second);
        }
        assert (!params.containsKey("sig"));
        String signature = generateSignature(FacebookSignatureUtil.convert(params.entrySet()),
                method.requiresSession());
        params.put("sig", signature);
        boolean doHttps = this.isDesktop() && FacebookMethod.AUTH_GET_SESSION.equals(method);
        InputStream data = method.takesFile()? postFileRequest(method.methodName(), params, /* doEncode */
                true): postRequest(method.methodName(), params, doHttps, /* doEncode */true);
        return parseCallResult(data, method);
    }

    /**
     * Parses the result of an API call into a T.
     * 
     * @param data an InputStream with the results of a request to the Facebook servers
     * @param method the method called
     * @throws FacebookException if <code>data</code> represents an error
     * @throws IOException if <code>data</code> is not readable
     * @return a T
     */
    protected abstract T parseCallResult(InputStream data, IFacebookMethod method)
            throws FacebookException, IOException;

    /**
     * Recaches the referenced url.
     * 
     * @param url the URL to refresh
     * @return boolean indicating whether the refresh succeeded
     */
    public boolean fbml_refreshRefUrl(URL url) throws FacebookException, IOException {
        return extractBoolean(this.callMethod(FacebookMethod.FBML_REFRESH_REF_URL,
                new Pair<String,CharSequence>("url", url.toString())));
    }

    /**
     * Retrieves the outstanding notifications for the session user.
     * 
     * @return a T containing notification count pairs for 'messages', 'pokes' and 'shares', a uid
     *         list of 'friend_requests', a gid list of 'group_invites', and an eid list of
     *         'event_invites'
     */
    public T notifications_get() throws FacebookException, IOException {
        return this.callMethod(FacebookMethod.NOTIFICATIONS_GET);
    }

    /**
     * Retrieves the requested info fields for the requested set of users.
     * 
     * @param userIds a collection of user IDs for which to fetch info
     * @param fields a set of ProfileFields
     * @return a T consisting of a list of users, with each user element containing the requested
     *         fields.
     */
    public T users_getInfo(Collection<Integer> userIds, EnumSet<ProfileField> fields)
            throws FacebookException, IOException {
        // assertions test for invalid params
        assert (userIds != null);
        assert (fields != null);
        assert (!fields.isEmpty());
        return this.callMethod(FacebookMethod.USERS_GET_INFO, new Pair<String,CharSequence>("uids",
                delimit(userIds)), new Pair<String,CharSequence>("fields", delimit(fields)));
    }

    /**
     * Retrieves the user ID of the user logged in to this API session
     * 
     * @return the Facebook user ID of the logged-in user
     */
    public int users_getLoggedInUser() throws FacebookException, IOException {
        T result = this.callMethod(FacebookMethod.USERS_GET_LOGGED_IN_USER);
        return extractInt(result);
    }

    /**
     * Call this function to get the user ID.
     * 
     * @return The ID of the current session's user, or -1 if none.
     */
    public int auth_getUserId(String authToken) throws FacebookException, IOException {
        /*
         * Get the session information if we don't have it; this will populate the user ID as well.
         */
        if (null == this._sessionKey)
            auth_getSession(authToken);
        return this._userId;
    }

    public boolean isDesktop() {
        return this._isDesktop;
    }

    private boolean photos_addTag(Long photoId, Double xPct, Double yPct, Integer taggedUserId,
            CharSequence tagText) throws FacebookException, IOException {
        assert (null != photoId && !photoId.equals(0));
        assert (null != taggedUserId || null != tagText);
        assert (null != xPct && xPct >= 0 && xPct <= 100);
        assert (null != yPct && yPct >= 0 && yPct <= 100);
        T d = this.callMethod(FacebookMethod.PHOTOS_ADD_TAG, new Pair<String,CharSequence>("pid",
                photoId.toString()), new Pair<String,CharSequence>("tag_uid", taggedUserId
                .toString()), new Pair<String,CharSequence>("x", xPct.toString()),
                new Pair<String,CharSequence>("y", yPct.toString()));
        return extractBoolean(d);
    }

    /**
     * Retrieves an indicator of whether the logged-in user has installed the application associated
     * with the _apiKey.
     * 
     * @return boolean indicating whether the user has installed the app
     */
    public boolean users_isAppAdded() throws FacebookException, IOException {
        return extractBoolean(this.callMethod(FacebookMethod.USERS_IS_APP_ADDED));
    }

    /**
     * Retrieves whether the logged-in user has granted the specified permission to this
     * application.
     * 
     * @param permission an extended permission (e.g. FacebookExtendedPerm.MARKETPLACE,
     *            "photo_upload")
     * @return boolean indicating whether the user has the permission
     * @see FacebookExtendedPerm
     * @see <a href="http://wiki.developers.facebook.com/index.php/Users.hasAppPermission">
     *      Developers Wiki: Users.hasAppPermission</a>
     */
    public boolean users_hasAppPermission(CharSequence permission) throws FacebookException,
            IOException {
        return extractBoolean(this.callMethod(FacebookMethod.USERS_HAS_APP_PERMISSION,
                new Pair<String,CharSequence>("ext_perm", permission)));
    }

    /**
     * Sets the logged-in user's Facebook status. Requires the status_update extended permission.
     * 
     * @return whether the status was successfully set
     * @see #users_hasAppPermission
     * @see FacebookExtendedPerm#STATUS_UPDATE
     * @see <a href="http://wiki.developers.facebook.com/index.php/Users.setStatus"> Developers
     *      Wiki: Users.setStatus</a>
     */
    public boolean users_setStatus(String status) throws FacebookException, IOException {
        return extractBoolean(this.callMethod(FacebookMethod.USERS_SET_STATUS,
                new Pair<String,CharSequence>("status", status)));
    }

    /**
     * Clears the logged-in user's Facebook status. Requires the status_update extended permission.
     * 
     * @return whether the status was successfully cleared
     * @see #users_hasAppPermission
     * @see FacebookExtendedPerm#STATUS_UPDATE
     * @see <a href="http://wiki.developers.facebook.com/index.php/Users.setStatus"> Developers
     *      Wiki: Users.setStatus</a>
     */
    public boolean users_clearStatus() throws FacebookException, IOException {
        return extractBoolean(this.callMethod(FacebookMethod.USERS_SET_STATUS,
                new Pair<String,CharSequence>("clear", "1")));
    }

    /**
     * Adds a tag to a photo.
     * 
     * @param photoId The photo id of the photo to be tagged.
     * @param xPct The horizontal position of the tag, as a percentage from 0 to 100, from the left
     *            of the photo.
     * @param yPct The list of photos from which to extract photo tags.
     * @param tagText The text of the tag.
     * @return whether the tag was successfully added.
     */
    public boolean photos_addTag(Long photoId, CharSequence tagText, Double xPct, Double yPct)
            throws FacebookException, IOException {
        return photos_addTag(photoId, xPct, yPct, null, tagText);
    }

    /**
     * Helper function for posting a request that includes raw file data, eg
     * {@link #photos_upload(File)}.
     * 
     * @param methodName the name of the method
     * @param params request parameters (not including the file)
     * @return an InputStream with the request response
     * @see #photos_upload(File)
     */
    protected InputStream postFileRequest(String methodName, Map<String,CharSequence> params)
            throws IOException {
        return postFileRequest(methodName, params, /* doEncode */true);
    }

    /**
     * Helper function for posting a request that includes raw file data, eg
     * {@link #photos_upload(File)}.
     * 
     * @param methodName the name of the method
     * @param params request parameters (not including the file)
     * @param doEncode whether to UTF8-encode the parameters
     * @return an InputStream with the request response
     * @see #photos_upload(File)
     */
    protected InputStream postFileRequest(String methodName, Map<String,CharSequence> params,
            boolean doEncode) throws IOException {
        assert (null != _uploadFile);
        try {
            BufferedInputStream bufin = new BufferedInputStream(new FileInputStream(_uploadFile));
            String boundary = Long.toString(System.currentTimeMillis(), 16);
            URLConnection con = SERVER_URL.openConnection();
            con.setDoInput(true);
            con.setDoOutput(true);
            con.setUseCaches(false);
            con.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
            con.setRequestProperty("MIME-version", "1.0");
            DataOutputStream out = new DataOutputStream(con.getOutputStream());
            for (Map.Entry<String,CharSequence> entry: params.entrySet()) {
                out.writeBytes(PREF + boundary + CRLF);
                out.writeBytes("Content-disposition: form-data; name=\"" + entry.getKey() + "\"");
                out.writeBytes(CRLF + CRLF);
                out.writeBytes(doEncode? encode(entry.getValue()): entry.getValue().toString());
                out.writeBytes(CRLF);
            }
            out.writeBytes(PREF + boundary + CRLF);
            out.writeBytes("Content-disposition: form-data; filename=\"" + _uploadFile.getName()
                    + "\"" + CRLF);
            out.writeBytes("Content-Type: image/jpeg" + CRLF);
            // out.writeBytes("Content-Transfer-Encoding: binary" + CRLF); // not necessary
            // Write the file
            out.writeBytes(CRLF);
            byte b[] = new byte[UPLOAD_BUFFER_SIZE];
            int byteCounter = 0;
            int i;
            while (-1 != (i = bufin.read(b))) {
                byteCounter += i;
                out.write(b, 0, i);
            }
            out.writeBytes(CRLF + PREF + boundary + PREF + CRLF);
            out.flush();
            out.close();
            InputStream is = con.getInputStream();
            return is;
        } catch (Exception e) {
            logException(e);
            return null;
        }
    }

    /**
     * Logs an exception with default message
     * 
     * @param e the exception
     */
    protected final void logException(Exception e) {
        logException("exception", e);
    }

    /**
     * Logs an exception with an introductory message in addition to the exception's getMessage().
     * 
     * @param msg message
     * @param e exception
     * @see Exception#getMessage
     */
    protected void logException(CharSequence msg, Exception e) {
        System.err.println(msg + ":" + e.getMessage());
        e.printStackTrace();
    }

    /**
     * Logs a message. Override this for more detailed logging.
     * 
     * @param message
     */
    protected void log(CharSequence message) {
        System.out.println(message);
    }

    /**
     * @return whether debugging is activated
     */
    public boolean isDebug() {
        return (null == _debug)? DEBUG: _debug.booleanValue();
    }

    /**
     * Send a notification message to the specified users on behalf of the logged-in user.
     * 
     * @param recipientIds the user ids to which the message is to be sent. if empty, notification
     *            will be sent to logged-in user.
     * @param notification the FBML to be displayed on the notifications page; only a stripped-down
     *            set of FBML tags that result in text and links is allowed
     * @return a URL, possibly null, to which the user should be redirected to finalize the sending
     *         of the email
     * @see <a href="http://wiki.developers.facebook.com/index.php/Notifications.sendEmail">
     *      Developers Wiki: notifications.send</a>
     */
    public void notifications_send(Collection<Integer> recipientIds, CharSequence notification)
            throws FacebookException, IOException {
        assert (null != notification);
        ArrayList<Pair<String,CharSequence>> args = new ArrayList<Pair<String,CharSequence>>(3);
        if (null != recipientIds && !recipientIds.isEmpty()) {
            args.add(new Pair<String,CharSequence>("to_ids", delimit(recipientIds)));
        }
        args.add(new Pair<String,CharSequence>("notification", notification));
        this.callMethod(FacebookMethod.NOTIFICATIONS_SEND, args);
    }

    /**
     * Send a notification message to the logged-in user.
     * 
     * @param notification the FBML to be displayed on the notifications page; only a stripped-down
     *            set of FBML tags that result in text and links is allowed
     * @return a URL, possibly null, to which the user should be redirected to finalize the sending
     *         of the email
     * @see <a href="http://wiki.developers.facebook.com/index.php/Notifications.sendEmail">
     *      Developers Wiki: notifications.send</a>
     */
    public void notifications_send(CharSequence notification) throws FacebookException, IOException {
        notifications_send(/* recipients */null, notification);
    }

    /**
     * Sends a notification email to the specified users, who must have added your application. You
     * can send five (5) emails to a user per day. Requires a session key for desktop applications,
     * which may only send email to the person whose session it is. This method does not require a
     * session for Web applications. Either <code>fbml</code> or <code>text</code> must be
     * specified.
     * 
     * @param recipientIds up to 100 user ids to which the message is to be sent
     * @param subject the subject of the notification email (optional)
     * @param fbml markup to be sent to the specified users via email; only a stripped-down set of
     *            FBML tags that result in text, links and linebreaks is allowed
     * @param text the plain text to send to the specified users via email
     * @return a comma-separated list of the IDs of the users to whom the email was successfully
     *         sent
     * @see <a href="http://wiki.developers.facebook.com/index.php/Notifications.send"> Developers
     *      Wiki: notifications.sendEmail</a>
     */
    public String notifications_sendEmail(Collection<Integer> recipientIds, CharSequence subject,
            CharSequence fbml, CharSequence text) throws FacebookException, IOException {
        if (null == recipientIds || recipientIds.isEmpty()) {
            throw new IllegalArgumentException("List of email recipients cannot be empty");
        }
        boolean hasText = null != text && (0 != text.length());
        boolean hasFbml = null != fbml && (0 != fbml.length());
        if (!hasText && !hasFbml) {
            throw new IllegalArgumentException("Text and/or fbml must not be empty");
        }
        ArrayList<Pair<String,CharSequence>> args = new ArrayList<Pair<String,CharSequence>>(4);
        args.add(new Pair<String,CharSequence>("recipients", delimit(recipientIds)));
        args.add(new Pair<String,CharSequence>("subject", subject));
        if (hasText) {
            args.add(new Pair<String,CharSequence>("text", text));
        }
        if (hasFbml) {
            args.add(new Pair<String,CharSequence>("fbml", fbml));
        }
        // this method requires a session only if we're dealing with a desktop app
        T result = this.callMethod(this.isDesktop()
                ? FacebookMethod.NOTIFICATIONS_SEND_EMAIL
                : FacebookMethod.NOTIFICATIONS_SEND_EMAIL, args);
        return extractString(result);
    }

    /**
     * Sends a notification email to the specified users, who must have added your application. You
     * can send five (5) emails to a user per day. Requires a session key for desktop applications,
     * which may only send email to the person whose session it is. This method does not require a
     * session for Web applications.
     * 
     * @param recipientIds up to 100 user ids to which the message is to be sent
     * @param subject the subject of the notification email (optional)
     * @param fbml markup to be sent to the specified users via email; only a stripped-down set of
     *            FBML that allows only tags that result in text, links and linebreaks is allowed
     * @return a comma-separated list of the IDs of the users to whom the email was successfully
     *         sent
     * @see <a href="http://wiki.developers.facebook.com/index.php/Notifications.send"> Developers
     *      Wiki: notifications.sendEmail</a>
     */
    public String notifications_sendEmail(Collection<Integer> recipientIds, CharSequence subject,
            CharSequence fbml) throws FacebookException, IOException {
        return notifications_sendEmail(recipientIds, subject, fbml, /* text */null);
    }

    /**
     * Sends a notification email to the specified users, who must have added your application. You
     * can send five (5) emails to a user per day. Requires a session key for desktop applications,
     * which may only send email to the person whose session it is. This method does not require a
     * session for Web applications.
     * 
     * @param recipientIds up to 100 user ids to which the message is to be sent
     * @param subject the subject of the notification email (optional)
     * @param text the plain text to send to the specified users via email
     * @return a comma-separated list of the IDs of the users to whom the email was successfully
     *         sent
     * @see <a href="http://wiki.developers.facebook.com/index.php/Notifications.sendEmail">
     *      Developers Wiki: notifications.sendEmail</a>
     */
    public String notifications_sendEmailPlain(Collection<Integer> recipientIds,
            CharSequence subject, CharSequence text) throws FacebookException, IOException {
        return notifications_sendEmail(recipientIds, subject, /* fbml */null, text);
    }

    /**
     * Extracts a URL from a result that consists of a URL only.
     * 
     * @param result
     * @return the URL
     */
    protected abstract URL extractURL(T result) throws IOException;

    /**
     * Recaches the image with the specified imageUrl.
     * 
     * @param imageUrl String representing the image URL to refresh
     * @return boolean indicating whether the refresh succeeded
     */
    public boolean fbml_refreshImgSrc(String imageUrl) throws FacebookException, IOException {
        return fbml_refreshImgSrc(new URL(imageUrl));
    }

    /**
     * Uploads a photo to Facebook.
     * 
     * @param photo an image file
     * @return a T with the standard Facebook photo information
     * @see <a href="http://wiki.developers.facebook.com/index.php/Photos.upload"> Developers wiki:
     *      Photos.upload</a>
     */
    public T photos_upload(File photo) throws FacebookException, IOException {
        return photos_upload(photo, /* caption */null, /* albumId */null);
    }

    /**
     * Uploads a photo to Facebook.
     * 
     * @param photo an image file
     * @param caption a description of the image contents
     * @return a T with the standard Facebook photo information
     * @see <a href="http://wiki.developers.facebook.com/index.php/Photos.upload"> Developers wiki:
     *      Photos.upload</a>
     */
    public T photos_upload(File photo, String caption) throws FacebookException, IOException {
        return photos_upload(photo, caption, /* albumId */null);
    }

    /**
     * Uploads a photo to Facebook.
     * 
     * @param photo an image file
     * @param albumId the album into which the photo should be uploaded
     * @return a T with the standard Facebook photo information
     * @see <a href="http://wiki.developers.facebook.com/index.php/Photos.upload"> Developers wiki:
     *      Photos.upload</a>
     */
    public T photos_upload(File photo, Long albumId) throws FacebookException, IOException {
        return photos_upload(photo, /* caption */null, albumId);
    }

    /**
     * Uploads a photo to Facebook.
     * 
     * @param photo an image file
     * @param caption a description of the image contents
     * @param albumId the album into which the photo should be uploaded
     * @return a T with the standard Facebook photo information
     * @see <a href="http://wiki.developers.facebook.com/index.php/Photos.upload"> Developers wiki:
     *      Photos.upload</a>
     */
    public T photos_upload(File photo, String caption, Long albumId) throws FacebookException,
            IOException {
        ArrayList<Pair<String,CharSequence>> params = new ArrayList<Pair<String,CharSequence>>(
                FacebookMethod.PHOTOS_UPLOAD.numParams());
        assert (photo.exists() && photo.canRead());
        this._uploadFile = photo;
        if (null != albumId)
            params.add(new Pair<String,CharSequence>("aid", Long.toString(albumId)));
        if (null != caption)
            params.add(new Pair<String,CharSequence>("caption", caption));
        return callMethod(FacebookMethod.PHOTOS_UPLOAD, params);
    }

    /**
     * Creates an album.
     * 
     * @param albumName The list of photos from which to extract photo tags.
     * @return the created album
     */
    public T photos_createAlbum(String albumName) throws FacebookException, IOException {
        return this.photos_createAlbum(albumName, null, /* description */null) /* location */;
    }

    /**
     * Adds a tag to a photo.
     * 
     * @param photoId The photo id of the photo to be tagged.
     * @param xPct The horizontal position of the tag, as a percentage from 0 to 100, from the left
     *            of the photo.
     * @param yPct The vertical position of the tag, as a percentage from 0 to 100, from the top of
     *            the photo.
     * @param taggedUserId The list of photos from which to extract photo tags.
     * @return whether the tag was successfully added.
     */
    public boolean photos_addTag(Long photoId, Integer taggedUserId, Double xPct, Double yPct)
            throws FacebookException, IOException {
        return photos_addTag(photoId, xPct, yPct, taggedUserId, null);
    }

    /**
     * Adds several tags to a photo.
     * 
     * @param photoId The photo id of the photo to be tagged.
     * @param tags A list of PhotoTags.
     * @return a list of booleans indicating whether the tag was successfully added.
     */
    public T photos_addTags(Long photoId, Collection<PhotoTag> tags) throws FacebookException,
            IOException {
        assert (photoId > 0);
        assert (null != tags && !tags.isEmpty());
        JSONArray jsonTags = new JSONArray();
        for (PhotoTag tag: tags) {
            jsonTags.add(tag.jsonify());
        }
        return this.callMethod(FacebookMethod.PHOTOS_ADD_TAG, new Pair<String,CharSequence>("pid",
                photoId.toString()), new Pair<String,CharSequence>("tags", jsonTags.toString()));
    }

    public void setIsDesktop(boolean isDesktop) {
        this._isDesktop = isDesktop;
    }

    /**
     * Returns all visible events according to the filters specified. This may be used to find all
     * events of a user, or to query specific eids.
     * 
     * @param eventIds filter by these event ID's (optional)
     * @param userId filter by this user only (optional)
     * @param startTime UTC lower bound (optional)
     * @param endTime UTC upper bound (optional)
     * @return T of events
     */
    public T events_get(Integer userId, Collection<Long> eventIds, Long startTime, Long endTime)
            throws FacebookException, IOException {
        ArrayList<Pair<String,CharSequence>> params = new ArrayList<Pair<String,CharSequence>>(
                FacebookMethod.EVENTS_GET.numParams());
        boolean hasUserId = null != userId && 0 != userId;
        boolean hasEventIds = null != eventIds && !eventIds.isEmpty();
        boolean hasStart = null != startTime && 0 != startTime;
        boolean hasEnd = null != endTime && 0 != endTime;
        if (hasUserId)
            params.add(new Pair<String,CharSequence>("uid", Integer.toString(userId)));
        if (hasEventIds)
            params.add(new Pair<String,CharSequence>("eids", delimit(eventIds)));
        if (hasStart)
            params.add(new Pair<String,CharSequence>("start_time", startTime.toString()));
        if (hasEnd)
            params.add(new Pair<String,CharSequence>("end_time", endTime.toString()));
        return this.callMethod(FacebookMethod.EVENTS_GET, params);
    }

    /**
     * Sets the FBML for a user's profile, including the content for both the profile box and the
     * profile actions.
     * 
     * @param userId the user whose profile FBML to set
     * @param fbmlMarkup refer to the FBML documentation for a description of the markup and its
     *            role in various contexts
     * @return a boolean indicating whether the FBML was successfully set
     * @deprecated Use {@link FacebookRestClient#profile_setFBML(CharSequence,CharSequence,Long)}
     *             instead.
     * @see #profile_setFBML(CharSequence,CharSequence,Long)
     */
    public boolean profile_setFBML(CharSequence fbmlMarkup, Integer userId)
            throws FacebookException, IOException {
        return extractBoolean(this.callMethod(FacebookMethod.PROFILE_SET_FBML,
                new Pair<String,CharSequence>("uid", Integer.toString(userId)),
                new Pair<String,CharSequence>("markup", fbmlMarkup)));
    }

    /**
     * Sets the FBML for a profile box on the logged-in user's profile.
     * 
     * @param fbmlMarkup refer to the FBML documentation for a description of the markup and its
     *            role in various contexts
     * @return a boolean indicating whether the FBML was successfully set
     * @see <a href="http://wiki.developers.facebook.com/index.php/Profile.setFBML"> Developers
     *      wiki: Profile.setFbml</a>
     */
    public boolean profile_setProfileFBML(CharSequence fbmlMarkup) throws FacebookException,
            IOException {
        return profile_setFBML(fbmlMarkup, /* profileActionFbmlMarkup */null, /* mobileFbmlMarkup */
                null,
                /* profileId */null);
    }

    /**
     * Sets the FBML for profile actions for the logged-in user.
     * 
     * @param fbmlMarkup refer to the FBML documentation for a description of the markup and its
     *            role in various contexts
     * @return a boolean indicating whether the FBML was successfully set
     * @see <a href="http://wiki.developers.facebook.com/index.php/Profile.setFBML"> Developers
     *      wiki: Profile.setFBML</a>
     */
    public boolean profile_setProfileActionFBML(CharSequence fbmlMarkup) throws FacebookException,
            IOException {
        return profile_setFBML( /* profileFbmlMarkup */null, fbmlMarkup, /* mobileFbmlMarkup */
                null,
                /* profileId */null);
    }

    /**
     * Sets the FBML for the logged-in user's profile on mobile devices.
     * 
     * @param fbmlMarkup refer to the FBML documentation for a description of the markup and its
     *            role in various contexts
     * @return a boolean indicating whether the FBML was successfully set
     * @see <a href="http://wiki.developers.facebook.com/index.php/Profile.setFBML"> Developers
     *      wiki: Profile.setFBML</a>
     */
    public boolean profile_setMobileFBML(CharSequence fbmlMarkup) throws FacebookException,
            IOException {
        return profile_setFBML( /* profileFbmlMarkup */null, /* profileActionFbmlMarkup */null,
                fbmlMarkup,
                /* profileId */null);
    }

    /**
     * Sets the FBML for a profile box on the user or page profile with ID <code>profileId</code>.
     * 
     * @param fbmlMarkup refer to the FBML documentation for a description of the markup and its
     *            role in various contexts
     * @param profileId a page or user ID (null for the logged-in user)
     * @return a boolean indicating whether the FBML was successfully set
     * @see <a href="http://wiki.developers.facebook.com/index.php/Profile.setFBML"> Developers
     *      wiki: Profile.setFbml</a>
     */
    public boolean profile_setProfileFBML(CharSequence fbmlMarkup, Long profileId)
            throws FacebookException, IOException {
        return profile_setFBML(fbmlMarkup, /* profileActionFbmlMarkup */null, /* mobileFbmlMarkup */
                null, profileId);
    }

    /**
     * Sets the FBML for profile actions for the user or page profile with ID <code>profileId</code>.
     * 
     * @param fbmlMarkup refer to the FBML documentation for a description of the markup and its
     *            role in various contexts
     * @param profileId a page or user ID (null for the logged-in user)
     * @return a boolean indicating whether the FBML was successfully set
     * @see <a href="http://wiki.developers.facebook.com/index.php/Profile.setFBML"> Developers
     *      wiki: Profile.setFBML</a>
     */
    public boolean profile_setProfileActionFBML(CharSequence fbmlMarkup, Long profileId)
            throws FacebookException, IOException {
        return profile_setFBML( /* profileFbmlMarkup */null, fbmlMarkup, /* mobileFbmlMarkup */
                null, profileId);
    }

    /**
     * Sets the FBML for the user or page profile with ID <code>profileId</code> on mobile
     * devices.
     * 
     * @param fbmlMarkup refer to the FBML documentation for a description of the markup and its
     *            role in various contexts
     * @param profileId a page or user ID (null for the logged-in user)
     * @return a boolean indicating whether the FBML was successfully set
     * @see <a href="http://wiki.developers.facebook.com/index.php/Profile.setFBML"> Developers
     *      wiki: Profile.setFBML</a>
     */
    public boolean profile_setMobileFBML(CharSequence fbmlMarkup, Long profileId)
            throws FacebookException, IOException {
        return profile_setFBML( /* profileFbmlMarkup */null, /* profileActionFbmlMarkup */null,
                fbmlMarkup, profileId);
    }

    /**
     * Sets the FBML for the profile box and profile actions for the logged-in user. Refer to the
     * FBML documentation for a description of the markup and its role in various contexts.
     * 
     * @param profileFbmlMarkup the FBML for the profile box
     * @param profileActionFbmlMarkup the FBML for the profile actions
     * @return a boolean indicating whether the FBML was successfully set
     * @see <a href="http://wiki.developers.facebook.com/index.php/Profile.setFBML"> Developers
     *      wiki: Profile.setFBML</a>
     */
    public boolean profile_setFBML(CharSequence profileFbmlMarkup,
            CharSequence profileActionFbmlMarkup) throws FacebookException, IOException {
        return profile_setFBML(profileFbmlMarkup, profileActionFbmlMarkup, /* mobileFbmlMarkup */
                null, /* profileId */null);
    }

    /**
     * Sets the FBML for the profile box and profile actions for the user or page profile with ID
     * <code>profileId</code>. Refer to the FBML documentation for a description of the markup
     * and its role in various contexts.
     * 
     * @param profileFbmlMarkup the FBML for the profile box
     * @param profileActionFbmlMarkup the FBML for the profile actions
     * @param profileId a page or user ID (null for the logged-in user)
     * @return a boolean indicating whether the FBML was successfully set
     * @see <a href="http://wiki.developers.facebook.com/index.php/Profile.setFBML"> Developers
     *      wiki: Profile.setFBML</a>
     */
    public boolean profile_setFBML(CharSequence profileFbmlMarkup,
            CharSequence profileActionFbmlMarkup, Long profileId) throws FacebookException,
            IOException {
        return profile_setFBML(profileFbmlMarkup, profileActionFbmlMarkup, /* mobileFbmlMarkup */
                null, profileId);
    }

    /**
     * Sets the FBML for the profile box, profile actions, and mobile devices for the logged-in
     * user. Refer to the FBML documentation for a description of the markup and its role in various
     * contexts.
     * 
     * @param profileFbmlMarkup the FBML for the profile box
     * @param profileActionFbmlMarkup the FBML for the profile actions
     * @param mobileFbmlMarkup the FBML for mobile devices
     * @return a boolean indicating whether the FBML was successfully set
     * @see <a href="http://wiki.developers.facebook.com/index.php/Profile.setFBML"> Developers
     *      wiki: Profile.setFBML</a>
     */
    public boolean profile_setFBML(CharSequence profileFbmlMarkup,
            CharSequence profileActionFbmlMarkup, CharSequence mobileFbmlMarkup)
            throws FacebookException, IOException {
        return profile_setFBML(profileFbmlMarkup, profileActionFbmlMarkup, mobileFbmlMarkup, /* profileId */
                null);
    }

    /**
     * Sets the FBML for the profile box, profile actions, and mobile devices for the user or page
     * profile with ID <code>profileId</code>. Refer to the FBML documentation for a description
     * of the markup and its role in various contexts.
     * 
     * @param profileFbmlMarkup the FBML for the profile box
     * @param profileActionFbmlMarkup the FBML for the profile actions
     * @param mobileFbmlMarkup the FBML for mobile devices
     * @param profileId a page or user ID (null for the logged-in user)
     * @return a boolean indicating whether the FBML was successfully set
     * @see <a href="http://wiki.developers.facebook.com/index.php/Profile.setFBML"> Developers
     *      wiki: Profile.setFBML</a>
     */
    public boolean profile_setFBML(CharSequence profileFbmlMarkup,
            CharSequence profileActionFbmlMarkup, CharSequence mobileFbmlMarkup, Long profileId)
            throws FacebookException, IOException {
        if (null == profileFbmlMarkup && null == profileActionFbmlMarkup
                && null == mobileFbmlMarkup) {
            throw new IllegalArgumentException(
                    "At least one of the FBML parameters must be provided");
        }
        if (this.isDesktop() && (null != profileId && this._userId != profileId)) {
            throw new IllegalArgumentException("Can't set FBML for another user from desktop app");
        }
        FacebookMethod method = FacebookMethod.PROFILE_SET_FBML;
        ArrayList<Pair<String,CharSequence>> params = new ArrayList<Pair<String,CharSequence>>(
                method.numParams());
        if (null != profileId && !this.isDesktop())
            params.add(new Pair<String,CharSequence>("uid", profileId.toString()));
        if (null != profileFbmlMarkup)
            params.add(new Pair<String,CharSequence>("profile", profileFbmlMarkup));
        if (null != profileActionFbmlMarkup)
            params.add(new Pair<String,CharSequence>("profile_action", profileActionFbmlMarkup));
        if (null != mobileFbmlMarkup)
            params.add(new Pair<String,CharSequence>("mobile_fbml", mobileFbmlMarkup));
        return extractBoolean(this.callMethod(method, params));
    }

    /**
     * Associates a "<code>handle</code>" with FBML markup so that the handle can be used within
     * the <a href="http://wiki.developers.facebook.com/index.php/Fb:ref">fb:ref</a> FBML tag. A
     * handle is unique within an application and allows an application to publish identical FBML to
     * many user profiles and do subsequent updates without having to republish FBML for each user.
     * 
     * @param handle a string, unique within the application, that
     * @param fbmlMarkup refer to the FBML documentation for a description of the markup and its
     *            role in various contexts
     * @return a boolean indicating whether the FBML was successfully set
     * @see <a href="http://wiki.developers.facebook.com/index.php/Fbml.setRefHandle"> Developers
     *      Wiki: Fbml.setRefHandle</a>
     */
    public boolean fbml_setRefHandle(CharSequence handle, CharSequence fbmlMarkup)
            throws FacebookException, IOException {
        return extractBoolean(this.callMethod(FacebookMethod.FBML_SET_REF_HANDLE,
                new Pair<String,CharSequence>("handle", handle), new Pair<String,CharSequence>(
                        "fbml", fbmlMarkup)));
    }

    /**
     * Determines whether this application can send SMS to the user identified by
     * <code>userId</code>
     * 
     * @param userId a user ID
     * @return true if sms can be sent to the user
     * @see FacebookExtendedPerm#SMS
     * @see <a
     *      href="http://wiki.developers.facebook.com/index.php/Mobile#Application_generated_messages">
     *      Developers Wiki: Mobile: Application Generated Messages</a>
     */
    public boolean sms_canSend(Integer userId) throws FacebookException, IOException {
        return extractBoolean(this.callMethod(FacebookMethod.SMS_CAN_SEND,
                new Pair<String,CharSequence>("uid", userId.toString())));
    }

    /**
     * Sends a message via SMS to the user identified by <code>userId</code> in response to a user
     * query associated with <code>mobileSessionId</code>.
     * 
     * @param userId a user ID
     * @param response the message to be sent via SMS
     * @param mobileSessionId the mobile session
     * @throws FacebookException in case of error
     * @throws IOException
     * @see FacebookExtendedPerm#SMS
     * @see <a
     *      href="http://wiki.developers.facebook.com/index.php/Mobile#Application_generated_messages">
     *      Developers Wiki: Mobile: Application Generated Messages</a>
     * @see <a href="http://wiki.developers.facebook.com/index.php/Mobile#Workflow"> Developers
     *      Wiki: Mobile: Workflow</a>
     */
    public void sms_sendResponse(Integer userId, CharSequence response, Integer mobileSessionId)
            throws FacebookException, IOException {
        this.callMethod(FacebookMethod.SMS_SEND_MESSAGE, new Pair<String,CharSequence>("uid",
                userId.toString()), new Pair<String,CharSequence>("message", response),
                new Pair<String,CharSequence>("session_id", mobileSessionId.toString()));
    }

    /**
     * Sends a message via SMS to the user identified by <code>userId</code>. The SMS extended
     * permission is required for success.
     * 
     * @param userId a user ID
     * @param message the message to be sent via SMS
     * @throws FacebookException in case of error
     * @throws IOException
     * @see FacebookExtendedPerm#SMS
     * @see <a
     *      href="http://wiki.developers.facebook.com/index.php/Mobile#Application_generated_messages">
     *      Developers Wiki: Mobile: Application Generated Messages</a>
     * @see <a href="http://wiki.developers.facebook.com/index.php/Mobile#Workflow"> Developers
     *      Wiki: Mobile: Workflow</a>
     */
    public void sms_sendMessage(Integer userId, CharSequence message) throws FacebookException,
            IOException {
        this.callMethod(FacebookMethod.SMS_SEND_MESSAGE, new Pair<String,CharSequence>("uid",
                userId.toString()), new Pair<String,CharSequence>("message", message),
                new Pair<String,CharSequence>("req_session", "0"));
    }

    /**
     * Sends a message via SMS to the user identified by <code>userId</code>, with the
     * expectation that the user will reply. The SMS extended permission is required for success.
     * The returned mobile session ID can be stored and used in {@link #sms_sendResponse} when the
     * user replies.
     * 
     * @param userId a user ID
     * @param message the message to be sent via SMS
     * @return a mobile session ID (can be used in {@link #sms_sendResponse})
     * @throws FacebookException in case of error, e.g. SMS is not enabled
     * @throws IOException
     * @see FacebookExtendedPerm#SMS
     * @see <a
     *      href="http://wiki.developers.facebook.com/index.php/Mobile#Application_generated_messages">
     *      Developers Wiki: Mobile: Application Generated Messages</a>
     * @see <a href="http://wiki.developers.facebook.com/index.php/Mobile#Workflow"> Developers
     *      Wiki: Mobile: Workflow</a>
     */
    public int sms_sendMessageWithSession(Integer userId, CharSequence message)
            throws FacebookException, IOException {
        return extractInt(this.callMethod(FacebookMethod.SMS_SEND_MESSAGE,
                new Pair<String,CharSequence>("uid", userId.toString()),
                new Pair<String,CharSequence>("message", message), new Pair<String,CharSequence>(
                        "req_session", "1")));
    }

    /**
     * Delimits a collection entries into a single CharSequence, using <code>delimiter</code> to
     * delimit each entry, and <code>equals</code> to delimit the key from the value inside each
     * entry.
     * 
     * @param entries
     * @param delimiter used to delimit one entry from another
     * @param equals used to delimit key from value
     * @param doEncode whether to encode the value of each entry
     * @return a CharSequence that contains all the entries, appropriately delimited
     */
    protected static CharSequence delimit(Collection<Map.Entry<String,CharSequence>> entries,
            CharSequence delimiter, CharSequence equals, boolean doEncode) {
        if (entries == null || entries.isEmpty())
            return null;
        StringBuilder buffer = new StringBuilder();
        boolean notFirst = false;
        for (Map.Entry<String,CharSequence> entry: entries) {
            if (notFirst)
                buffer.append(delimiter);
            else
                notFirst = true;
            CharSequence value = entry.getValue();
            buffer.append(entry.getKey()).append(equals).append(doEncode? encode(value): value);
        }
        return buffer;
    }

    /**
     * Creates an album.
     * 
     * @param name The album name.
     * @param location The album location (optional).
     * @param description The album description (optional).
     * @return an array of photo objects.
     */
    public T photos_createAlbum(String name, String description, String location)
            throws FacebookException, IOException {
        assert (null != name && !"".equals(name));
        ArrayList<Pair<String,CharSequence>> params = new ArrayList<Pair<String,CharSequence>>(
                FacebookMethod.PHOTOS_CREATE_ALBUM.numParams());
        params.add(new Pair<String,CharSequence>("name", name));
        if (null != description)
            params.add(new Pair<String,CharSequence>("description", description));
        if (null != location)
            params.add(new Pair<String,CharSequence>("location", location));
        return this.callMethod(FacebookMethod.PHOTOS_CREATE_ALBUM, params);
    }

    public void setDebug(boolean isDebug) {
        _debug = isDebug;
    }

    /**
     * Extracts a Boolean from a result that consists of a Boolean only.
     * 
     * @param result
     * @return the Boolean
     */
    protected boolean extractBoolean(T result) {
        return 1 == extractInt(result);
    }

    /**
     * Extracts an Integer from a result that consists of an Integer only.
     * 
     * @param result
     * @return the Integer
     */
    protected abstract int extractInt(T result);

    /**
     * Extracts an Long from a result that consists of a Long only.
     * 
     * @param result
     * @return the Long
     */
    protected abstract Long extractLong(T result);

    /**
     * Retrieves album metadata for a list of album IDs.
     * 
     * @param albumIds the ids of albums whose metadata is to be retrieved
     * @return album objects
     * @see <a href="http://wiki.developers.facebook.com/index.php/Photos.getAlbums"> Developers
     *      Wiki: Photos.getAlbums</a>
     */
    public T photos_getAlbums(Collection<Long> albumIds) throws FacebookException, IOException {
        return photos_getAlbums(null, /* userId */albumIds);
    }

    /**
     * Retrieves album metadata for albums owned by a user.
     * 
     * @param userId (optional) the id of the albums' owner (optional)
     * @return album objects
     * @see <a href="http://wiki.developers.facebook.com/index.php/Photos.getAlbums"> Developers
     *      Wiki: Photos.getAlbums</a>
     */
    public T photos_getAlbums(Integer userId) throws FacebookException, IOException {
        return photos_getAlbums(userId, null) /* albumIds */;
    }

    /**
     * Retrieves album metadata. Pass a user id and/or a list of album ids to specify the albums to
     * be retrieved (at least one must be provided)
     * 
     * @param userId (optional) the id of the albums' owner (optional)
     * @param albumIds (optional) the ids of albums whose metadata is to be retrieved
     * @return album objects
     * @see <a href="http://wiki.developers.facebook.com/index.php/Photos.getAlbums"> Developers
     *      Wiki: Photos.getAlbums</a>
     */
    public T photos_getAlbums(Integer userId, Collection<Long> albumIds) throws FacebookException,
            IOException {
        boolean hasUserId = null != userId && userId != 0;
        boolean hasAlbumIds = null != albumIds && !albumIds.isEmpty();
        assert (hasUserId || hasAlbumIds); // one of the two must be provided
        if (hasUserId)
            return (hasAlbumIds)? this.callMethod(FacebookMethod.PHOTOS_GET_ALBUMS,
                    new Pair<String,CharSequence>("uid", Integer.toString(userId)),
                    new Pair<String,CharSequence>("aids", delimit(albumIds))): this.callMethod(
                    FacebookMethod.PHOTOS_GET_ALBUMS, new Pair<String,CharSequence>("uid", Integer
                            .toString(userId)));
        else
            return this.callMethod(FacebookMethod.PHOTOS_GET_ALBUMS, new Pair<String,CharSequence>(
                    "aids", delimit(albumIds)));
    }

    /**
     * Recaches the image with the specified imageUrl.
     * 
     * @param imageUrl the image URL to refresh
     * @return boolean indicating whether the refresh succeeded
     */
    public boolean fbml_refreshImgSrc(URL imageUrl) throws FacebookException, IOException {
        return extractBoolean(this.callMethod(FacebookMethod.FBML_REFRESH_IMG_SRC,
                new Pair<String,CharSequence>("url", imageUrl.toString())));
    }

    /**
     * Retrieves the friends of the currently logged in user.
     * 
     * @return T of friends
     * @see <a href="http://wiki.developers.facebook.com/index.php/Friends.get"> Developers Wiki:
     *      Friends.get</a>
     */
    public T friends_get() throws FacebookException, IOException {
        return this.friends_get( /* friendListId */null);
    }

    /**
     * Retrieves the friends of the currently logged in user that are members of the friends list
     * with ID <code>friendListId</code>.
     * 
     * @param friendListId the friend list for which friends should be fetched. if <code>null</code>,
     *            all friends will be retrieved.
     * @return T of friends
     * @see <a href="http://wiki.developers.facebook.com/index.php/Friends.get"> Developers Wiki:
     *      Friends.get</a>
     */
    public T friends_get(Long friendListId) throws FacebookException, IOException {
        FacebookMethod method = FacebookMethod.FRIENDS_GET;
        Collection<Pair<String,CharSequence>> params = new ArrayList<Pair<String,CharSequence>>(
                method.numParams());
        if (null != friendListId) {
            if (0L >= friendListId) {
                throw new IllegalArgumentException("given invalid friendListId "
                        + friendListId.toString());
            }
            params.add(new Pair<String,CharSequence>("flid", friendListId.toString()));
        }
        return this.callMethod(method, params);
    }

    /**
     * Retrieves the friend lists of the currently logged in user.
     * 
     * @return T of friend lists
     * @see <a href="http://wiki.developers.facebook.com/index.php/Friends.getLists"> Developers
     *      Wiki: Friends.getLists</a>
     */
    public T friends_getLists() throws FacebookException, IOException {
        return this.callMethod(FacebookMethod.FRIENDS_GET_LISTS);
    }

    private InputStream postRequest(CharSequence method, Map<String,CharSequence> params,
            boolean doHttps, boolean doEncode) throws IOException {
        CharSequence buffer = (null == params)? "": delimit(params.entrySet(), "&", "=", doEncode);
        URL serverUrl = (doHttps)? HTTPS_SERVER_URL: _serverUrl;
        if (isDebug()) {
            StringBuilder debugMsg = new StringBuilder().append(method).append(" POST: ").append(
                    serverUrl.toString()).append("?");
            debugMsg.append(buffer);
            log(debugMsg);
        }
        HttpURLConnection conn = (HttpURLConnection)serverUrl.openConnection();
        try {
            conn.setRequestMethod("POST");
        } catch (ProtocolException ex) {
            logException(ex);
        }
        conn.setDoOutput(true);
        conn.connect();
        conn.getOutputStream().write(buffer.toString().getBytes());
        return conn.getInputStream();
    }

    /**
     * Call this function and store the result, using it to generate the appropriate login url and
     * then to retrieve the session information.
     * 
     * @return an authentication token
     */
    public String auth_createToken() throws FacebookException, IOException {
        T d = this.callMethod(FacebookMethod.AUTH_CREATE_TOKEN);
        return extractString(d);
    }

    /**
     * Extracts a String from a T consisting entirely of a String.
     * 
     * @param result
     * @return the String
     */
    protected abstract String extractString(T result);

    /**
     * Create a marketplace listing
     * 
     * @param showOnProfile whether the listing can be shown on the user's profile
     * @param attrs the properties of the listing
     * @return the id of the created listing
     * @see MarketplaceListing
     * @see <a href="http://wiki.developers.facebook.com/index.php/Marketplace.createListing">
     *      Developers Wiki: marketplace.createListing</a>
     */
    public Long marketplace_createListing(Boolean showOnProfile, MarketplaceListing attrs)
            throws FacebookException, IOException {
        T result = this.callMethod(FacebookMethod.MARKETPLACE_CREATE_LISTING,
                new Pair<String,CharSequence>("show_on_profile", showOnProfile? "1": "0"),
                new Pair<String,CharSequence>("listing_id", "0"), new Pair<String,CharSequence>(
                        "listing_attrs", attrs.jsonify().toString()));
        return this.extractLong(result);
    }

    /**
     * Modify a marketplace listing
     * 
     * @param listingId identifies the listing to be modified
     * @param showOnProfile whether the listing can be shown on the user's profile
     * @param attrs the properties of the listing
     * @return the id of the edited listing
     * @see MarketplaceListing
     * @see <a href="http://wiki.developers.facebook.com/index.php/Marketplace.createListing">
     *      Developers Wiki: marketplace.createListing</a>
     */
    public Long marketplace_editListing(Long listingId, Boolean showOnProfile,
            MarketplaceListing attrs) throws FacebookException, IOException {
        T result = this.callMethod(FacebookMethod.MARKETPLACE_CREATE_LISTING,
                new Pair<String,CharSequence>("show_on_profile", showOnProfile? "1": "0"),
                new Pair<String,CharSequence>("listing_id", listingId.toString()),
                new Pair<String,CharSequence>("listing_attrs", attrs.jsonify().toString()));
        return this.extractLong(result);
    }

    /**
     * Remove a marketplace listing
     * 
     * @param listingId the listing to be removed
     * @return boolean indicating whether the listing was removed
     * @see <a href="http://wiki.developers.facebook.com/index.php/Marketplace.removeListing">
     *      Developers Wiki: marketplace.removeListing</a>
     */
    public boolean marketplace_removeListing(Long listingId) throws FacebookException, IOException {
        return marketplace_removeListing(listingId, MARKETPLACE_STATUS_DEFAULT);
    }

    /**
     * Remove a marketplace listing
     * 
     * @param listingId the listing to be removed
     * @param status MARKETPLACE_STATUS_DEFAULT, MARKETPLACE_STATUS_SUCCESS, or
     *            MARKETPLACE_STATUS_NOT_SUCCESS
     * @return boolean indicating whether the listing was removed
     * @see <a href="http://wiki.developers.facebook.com/index.php/Marketplace.removeListing">
     *      Developers Wiki: marketplace.removeListing</a>
     */
    public boolean marketplace_removeListing(Long listingId, CharSequence status)
            throws FacebookException, IOException {
        assert MARKETPLACE_STATUS_DEFAULT.equals(status)
                || MARKETPLACE_STATUS_SUCCESS.equals(status)
                || MARKETPLACE_STATUS_NOT_SUCCESS.equals(status) : "Invalid status: " + status;
        T result = this.callMethod(FacebookMethod.MARKETPLACE_REMOVE_LISTING,
                new Pair<String,CharSequence>("listing_id", listingId.toString()),
                new Pair<String,CharSequence>("status", status));
        return this.extractBoolean(result);
    }

    /**
     * Get the categories available in marketplace.
     * 
     * @return a T listing the marketplace categories
     * @see <a href="http://wiki.developers.facebook.com/index.php/Marketplace.getCategories">
     *      Developers Wiki: marketplace.getCategories</a>
     */
    public T marketplace_getCategories() throws FacebookException, IOException {
        return this.callMethod(FacebookMethod.MARKETPLACE_GET_CATEGORIES);
    }

    /**
     * Get the subcategories available for a category.
     * 
     * @param category a category, e.g. "HOUSING"
     * @return a T listing the marketplace sub-categories
     * @see <a href="http://wiki.developers.facebook.com/index.php/Marketplace.getSubCategories">
     *      Developers Wiki: marketplace.getSubCategories</a>
     */
    public T marketplace_getSubCategories(CharSequence category) throws FacebookException,
            IOException {
        return this.callMethod(FacebookMethod.MARKETPLACE_GET_SUBCATEGORIES,
                new Pair<String,CharSequence>("category", category));
    }

    /**
     * Fetch marketplace listings, filtered by listing IDs and/or the posting users' IDs.
     * 
     * @param listingIds listing identifiers (required if uids is null/empty)
     * @param userIds posting user identifiers (required if listingIds is null/empty)
     * @return a T of marketplace listings
     * @see <a href="http://wiki.developers.facebook.com/index.php/Marketplace.getListings">
     *      Developers Wiki: marketplace.getListings</a>
     */
    public T marketplace_getListings(Collection<Long> listingIds, Collection<Integer> userIds)
            throws FacebookException, IOException {
        ArrayList<Pair<String,CharSequence>> params = new ArrayList<Pair<String,CharSequence>>(
                FacebookMethod.MARKETPLACE_GET_LISTINGS.numParams());
        if (null != listingIds && !listingIds.isEmpty()) {
            params.add(new Pair<String,CharSequence>("listing_ids", delimit(listingIds)));
        }
        if (null != userIds && !userIds.isEmpty()) {
            params.add(new Pair<String,CharSequence>("uids", delimit(userIds)));
        }
        assert !params.isEmpty() : "Either listingIds or userIds should be provided";
        return this.callMethod(FacebookMethod.MARKETPLACE_GET_LISTINGS, params);
    }

    /**
     * Search for marketplace listings, optionally by category, subcategory, and/or query string.
     * 
     * @param category the category of listings desired (optional except if subcategory is provided)
     * @param subCategory the subcategory of listings desired (optional)
     * @param query a query string (optional)
     * @return a T of marketplace listings
     * @see <a href="http://wiki.developers.facebook.com/index.php/Marketplace.search"> Developers
     *      Wiki: marketplace.search</a>
     */
    public T marketplace_search(CharSequence category, CharSequence subCategory, CharSequence query)
            throws FacebookException, IOException {
        ArrayList<Pair<String,CharSequence>> params = new ArrayList<Pair<String,CharSequence>>(
                FacebookMethod.MARKETPLACE_SEARCH.numParams());
        if (null != category && !"".equals(category)) {
            params.add(new Pair<String,CharSequence>("category", category));
            if (null != subCategory && !"".equals(subCategory)) {
                params.add(new Pair<String,CharSequence>("subcategory", subCategory));
            }
        }
        if (null != query && !"".equals(query)) {
            params.add(new Pair<String,CharSequence>("query", category));
        }
        return this.callMethod(FacebookMethod.MARKETPLACE_SEARCH, params);
    }

    /**
     * Retrieves the requested profile fields for the Facebook Pages with the given
     * <code>pageIds</code>. Can be called for pages that have added the application without
     * establishing a session.
     * 
     * @param pageIds the page IDs
     * @param fields a set of page profile fields
     * @return a T consisting of a list of pages, with each page element containing the requested
     *         fields.
     * @see <a href="http://wiki.developers.facebook.com/index.php/Pages.getInfo"> Developers Wiki:
     *      Pages.getInfo</a>
     */
    public T pages_getInfo(Collection<Long> pageIds, EnumSet<PageProfileField> fields)
            throws FacebookException, IOException {
        if (pageIds == null || pageIds.isEmpty()) {
            throw new IllegalArgumentException("pageIds cannot be empty or null");
        }
        if (fields == null || fields.isEmpty()) {
            throw new IllegalArgumentException("fields cannot be empty or null");
        }
        IFacebookMethod method = null == this._sessionKey
                ? FacebookMethod.PAGES_GET_INFO_NO_SESSION
                : FacebookMethod.PAGES_GET_INFO;
        return this.callMethod(method, new Pair<String,CharSequence>("page_ids", delimit(pageIds)),
                new Pair<String,CharSequence>("fields", delimit(fields)));
    }

    /**
     * Retrieves the requested profile fields for the Facebook Pages with the given
     * <code>pageIds</code>. Can be called for pages that have added the application without
     * establishing a session.
     * 
     * @param pageIds the page IDs
     * @param fields a set of page profile fields
     * @return a T consisting of a list of pages, with each page element containing the requested
     *         fields.
     * @see <a href="http://wiki.developers.facebook.com/index.php/Pages.getInfo"> Developers Wiki:
     *      Pages.getInfo</a>
     */
    public T pages_getInfo(Collection<Long> pageIds, Set<CharSequence> fields)
            throws FacebookException, IOException {
        if (pageIds == null || pageIds.isEmpty()) {
            throw new IllegalArgumentException("pageIds cannot be empty or null");
        }
        if (fields == null || fields.isEmpty()) {
            throw new IllegalArgumentException("fields cannot be empty or null");
        }
        IFacebookMethod method = null == this._sessionKey
                ? FacebookMethod.PAGES_GET_INFO_NO_SESSION
                : FacebookMethod.PAGES_GET_INFO;
        return this.callMethod(method, new Pair<String,CharSequence>("page_ids", delimit(pageIds)),
                new Pair<String,CharSequence>("fields", delimit(fields)));
    }

    /**
     * Retrieves the requested profile fields for the Facebook Pages of the user with the given
     * <code>userId</code>.
     * 
     * @param userId the ID of a user about whose pages to fetch info (defaulted to the logged-in
     *            user)
     * @param fields a set of PageProfileFields
     * @return a T consisting of a list of pages, with each page element containing the requested
     *         fields.
     * @see <a href="http://wiki.developers.facebook.com/index.php/Pages.getInfo"> Developers Wiki:
     *      Pages.getInfo</a>
     */
    public T pages_getInfo(Integer userId, EnumSet<PageProfileField> fields)
            throws FacebookException, IOException {
        if (fields == null || fields.isEmpty()) {
            throw new IllegalArgumentException("fields cannot be empty or null");
        }
        if (userId == null) {
            userId = this._userId;
        }
        return this.callMethod(FacebookMethod.PAGES_GET_INFO, new Pair<String,CharSequence>("uid",
                userId.toString()), new Pair<String,CharSequence>("fields", delimit(fields)));
    }

    /**
     * Retrieves the requested profile fields for the Facebook Pages of the user with the given
     * <code>userId</code>.
     * 
     * @param userId the ID of a user about whose pages to fetch info (defaulted to the logged-in
     *            user)
     * @param fields a set of page profile fields
     * @return a T consisting of a list of pages, with each page element containing the requested
     *         fields.
     * @see <a href="http://wiki.developers.facebook.com/index.php/Pages.getInfo"> Developers Wiki:
     *      Pages.getInfo</a>
     */
    public T pages_getInfo(Integer userId, Set<CharSequence> fields) throws FacebookException,
            IOException {
        if (fields == null || fields.isEmpty()) {
            throw new IllegalArgumentException("fields cannot be empty or null");
        }
        if (userId == null) {
            userId = this._userId;
        }
        return this.callMethod(FacebookMethod.PAGES_GET_INFO, new Pair<String,CharSequence>("uid",
                userId.toString()), new Pair<String,CharSequence>("fields", delimit(fields)));
    }

    /**
     * Checks whether a page has added the application
     * 
     * @param pageId the ID of the page
     * @return true if the page has added the application
     * @see <a href="http://wiki.developers.facebook.com/index.php/Pages.isAppAdded"> Developers
     *      Wiki: Pages.isAppAdded</a>
     */
    public boolean pages_isAppAdded(Long pageId) throws FacebookException, IOException {
        return extractBoolean(this.callMethod(FacebookMethod.PAGES_IS_APP_ADDED,
                new Pair<String,CharSequence>("page_id", pageId.toString())));
    }

    /**
     * Checks whether a user is a fan of the page with the given <code>pageId</code>.
     * 
     * @param pageId the ID of the page
     * @param userId the ID of the user (defaults to the logged-in user if null)
     * @return true if the user is a fan of the page
     * @see <a href="http://wiki.developers.facebook.com/index.php/Pages.isFan"> Developers Wiki:
     *      Pages.isFan</a>
     */
    public boolean pages_isFan(Long pageId, Integer userId) throws FacebookException, IOException {
        return extractBoolean(this.callMethod(FacebookMethod.PAGES_IS_FAN,
                new Pair<String,CharSequence>("page_id", pageId.toString()),
                new Pair<String,CharSequence>("uid", userId.toString())));
    }

    /**
     * Checks whether the logged-in user is a fan of the page with the given <code>pageId</code>.
     * 
     * @param pageId the ID of the page
     * @return true if the logged-in user is a fan of the page
     * @see <a href="http://wiki.developers.facebook.com/index.php/Pages.isFan"> Developers Wiki:
     *      Pages.isFan</a>
     */
    public boolean pages_isFan(Long pageId) throws FacebookException, IOException {
        return extractBoolean(this.callMethod(FacebookMethod.PAGES_IS_FAN,
                new Pair<String,CharSequence>("page_id", pageId.toString())));
    }

    /**
     * Checks whether the logged-in user for this session is an admin of the page with the given
     * <code>pageId</code>.
     * 
     * @param pageId the ID of the page
     * @return true if the logged-in user is an admin
     * @see <a href="http://wiki.developers.facebook.com/index.php/Pages.isAdmin"> Developers Wiki:
     *      Pages.isAdmin</a>
     */
    public boolean pages_isAdmin(Long pageId) throws FacebookException, IOException {
        return extractBoolean(this.callMethod(FacebookMethod.PAGES_IS_ADMIN,
                new Pair<String,CharSequence>("page_id", pageId.toString())));
    }

    /**
     * Sets several property values for an application. The properties available are analogous to
     * the ones editable via the Facebook Developer application. A session is not required to use
     * this method.
     * 
     * @param properties an ApplicationPropertySet that is translated into a single JSON String.
     * @return a boolean indicating whether the properties were successfully set
     */
    public boolean admin_setAppProperties(ApplicationPropertySet properties)
            throws FacebookException, IOException {
        if (null == properties || properties.isEmpty()) {
            throw new IllegalArgumentException(
                    "expecting a non-empty set of application properties");
        }
        return extractBoolean(this.callMethod(FacebookMethod.ADMIN_SET_APP_PROPERTIES,
                new Pair<String,CharSequence>("properties", properties.toJsonString())));
    }

    /**
     * Gets property values previously set for an application on either the Facebook Developer
     * application or the with the <code>admin.setAppProperties</code> call. A session is not
     * required to use this method.
     * 
     * @param properties an enumeration of the properties to get
     * @return an ApplicationPropertySet
     * @see ApplicationProperty
     * @see <a href="http://wiki.developers.facebook.com/index.php/Admin.getAppProperties">
     *      Developers Wiki: Admin.getAppProperties</a>
     */
    public ApplicationPropertySet admin_getAppProperties(EnumSet<ApplicationProperty> properties)
            throws FacebookException, IOException {
        if (null == properties || properties.isEmpty()) {
            throw new IllegalArgumentException(
                    "expecting a non-empty set of application properties");
        }
        JSONArray propList = new JSONArray();
        for (ApplicationProperty prop: properties) {
            propList.add(prop.propertyName());
        }
        String propJson = extractString(this.callMethod(FacebookMethod.ADMIN_GET_APP_PROPERTIES,
                new Pair<String,CharSequence>("properties", propList.toString())));
        return new ApplicationPropertySet(propJson);
    }

    /**
     * Retrieves all cookies for the application and the given <code>userId</code>. If a
     * <code>cookieName</code> is specified, only that cookie will be returned.
     * 
     * @param userId The user from whom to get the cookies (defaults to logged-in user).
     * @param cookieName The name of the cookie to get. If null, all cookies will be returned
     * @return a T of cookies.
     * @see <a href="http://wiki.developers.facebook.com/index.php/Data.getCookies"> Developers
     *      Wiki: Data.getCookies</a>
     * @see <a href="http://wiki.developers.facebook.com/index.php/Cookies"> Developers Wiki:
     *      Cookies</a>
     */
    public T data_getCookies(Integer userId, CharSequence cookieName) throws FacebookException,
            IOException {
        Collection<Pair<String,CharSequence>> params = new ArrayList<Pair<String,CharSequence>>(
                FacebookMethod.DATA_GET_COOKIES.numParams());
        params.add(new Pair<String,CharSequence>("uid", userId.toString()));
        if (null != cookieName) {
            params.add(new Pair<String,CharSequence>("name", cookieName.toString()));
        }
        return this.callMethod(FacebookMethod.DATA_GET_COOKIES, params);
    }

    /**
     * Retrieves all cookies for the application and the given <code>userId</code>.
     * 
     * @param userId The user from whom to get the cookies (defaults to logged-in user).
     * @return a T of cookies.
     * @see <a href="http://wiki.developers.facebook.com/index.php/Data.getCookies"> Developers
     *      Wiki: Data.getCookies</a>
     * @see <a href="http://wiki.developers.facebook.com/index.php/Cookies"> Developers Wiki:
     *      Cookies</a>
     */
    public T data_getCookies(Integer userId) throws FacebookException, IOException {
        return data_getCookies(userId, /* cookieName */null);
    }

    /**
     * Sets a cookie for a given user and application.
     * 
     * @param userId The user for whom this cookie needs to be set
     * @param cookieName Name of the cookie.
     * @param cookieValue Value of the cookie.
     * @return true if cookie was successfully set, false otherwise
     * @see <a href="http://wiki.developers.facebook.com/index.php/Data.getCookies"> Developers
     *      Wiki: Data.setCookie</a>
     * @see <a href="http://wiki.developers.facebook.com/index.php/Cookies"> Developers Wiki:
     *      Cookies</a>
     */
    public boolean data_setCookie(Integer userId, CharSequence cookieName, CharSequence cookieValue)
            throws FacebookException, IOException {
        return data_setCookie(userId, cookieName, cookieValue, /* expiresTimestamp */null, /* path */
                null);
    }

    /**
     * Sets a cookie for a given user and application.
     * 
     * @param userId The user for whom this cookie needs to be set
     * @param cookieName Name of the cookie.
     * @param cookieValue Value of the cookie.
     * @param path Path relative to the application's callback URL, with which the cookie should be
     *            associated. If null, defaulted to "/"
     * @return true if cookie was successfully set, false otherwise
     * @see <a href="http://wiki.developers.facebook.com/index.php/Data.getCookies"> Developers
     *      Wiki: Data.setCookie</a>
     * @see <a href="http://wiki.developers.facebook.com/index.php/Cookies"> Developers Wiki:
     *      Cookies</a>
     */
    public boolean data_setCookie(Integer userId, CharSequence cookieName,
            CharSequence cookieValue, CharSequence path) throws FacebookException, IOException {
        return data_setCookie(userId, cookieName, cookieValue, /* expiresTimestamp */null, path);
    }

    /**
     * Sets a cookie for a given user and application.
     * 
     * @param userId The user for whom this cookie needs to be set
     * @param cookieName Name of the cookie.
     * @param cookieValue Value of the cookie.
     * @param expiresTimestamp Time stamp when the cookie should expire. If not specified, the
     *            cookie never expires.
     * @return true if cookie was successfully set, false otherwise
     * @see <a href="http://wiki.developers.facebook.com/index.php/Data.getCookies"> Developers
     *      Wiki: Data.setCookie</a>
     * @see <a href="http://wiki.developers.facebook.com/index.php/Cookies"> Developers Wiki:
     *      Cookies</a>
     */
    public boolean data_setCookie(Integer userId, CharSequence cookieName,
            CharSequence cookieValue, Long expiresTimestamp) throws FacebookException, IOException {
        return data_setCookie(userId, cookieName, cookieValue, expiresTimestamp, /* path */null);
    }

    /**
     * Sets a cookie for a given user and application.
     * 
     * @param userId The user for whom this cookie needs to be set
     * @param cookieName Name of the cookie.
     * @param cookieValue Value of the cookie.
     * @param expiresTimestamp Time stamp when the cookie should expire. If not specified, the
     *            cookie never expires.
     * @return true if cookie was successfully set, false otherwise
     * @see <a href="http://wiki.developers.facebook.com/index.php/Data.getCookies"> Developers
     *      Wiki: Data.setCookie</a>
     * @see <a href="http://wiki.developers.facebook.com/index.php/Cookies"> Developers Wiki:
     *      Cookies</a>
     */
    public boolean data_setCookie(Integer userId, CharSequence cookieName,
            CharSequence cookieValue, Long expiresTimestamp, CharSequence path)
            throws FacebookException, IOException {
        if (null == userId || 0 >= userId)
            throw new IllegalArgumentException("userId should be provided.");
        if (null == cookieName || null == cookieValue)
            throw new IllegalArgumentException("cookieName and cookieValue should be provided.");
        ArrayList<Pair<String,CharSequence>> params = new ArrayList<Pair<String,CharSequence>>(
                FacebookMethod.DATA_GET_COOKIES.numParams());
        params.add(new Pair<String,CharSequence>("uid", userId.toString()));
        params.add(new Pair<String,CharSequence>("name", cookieName));
        params.add(new Pair<String,CharSequence>("value", cookieValue));
        if (null != expiresTimestamp && expiresTimestamp >= 0L)
            params.add(new Pair<String,CharSequence>("expires", expiresTimestamp.toString()));
        if (null != path)
            params.add(new Pair<String,CharSequence>("path", path));
        return extractBoolean(this.callMethod(FacebookMethod.DATA_GET_COOKIES, params));
    }
}
